package com.kl.lock;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * redis实现可重入分布式锁
 */
@Component
@Slf4j
public class RedisLockImpl implements RedisLock, Serializable {

    private final RedisTemplate<String, String> redisTemplate;

    public RedisLockImpl(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public Lock getLock(String key) {
        return new LockImpl(key);
    }

    class LockImpl implements Lock {
        private static final String lockStr = "local key = KEYS[1]\n" +
                "local lockKey = ARGV[1]\n" +
                "local lockCount = 1\n" +
                "\n" +
                "\n" +
                "local val = redis.call('hget',key,lockKey)\n" +
                "if val then\n" +
                "    redis.call('hincrby',key,lockKey,1)\n" +
                "    return 1\n" +
                "end\n" +
                "if redis.call('exists',key)==0 then\n" +
                "    redis.call('hset',key,lockKey,1)\n" +
                "    redis.call('expire',lockKey,5)\n" +
                "    return 1\n" +
                "end\n" +
                "return 0";

        private static final String unLockStr = "--释放锁\n" +
                "local key = KEYS[1]\n" +
                "local lockKey = ARGV[1]\n" +
                "local value = redis.call('hget',key,lockKey)\n" +
                "if value then\n" +
                "    if tonumber(value)>1 then\n" +
                "        redis.call('hincrby',key,lockKey,-1)\n" +
                "    else\n" +
                "        redis.call('del',key)\n" +
                "    end\n" +
                "end";

        //key过期时间
        private static final long keyTimeOut = 30000;

        private static final long SLEEP_TIME = 50;

        private final DefaultRedisScript<Long> lockScript = new DefaultRedisScript<>(lockStr, Long.class);

        private final DefaultRedisScript<Long> unLockScript = new DefaultRedisScript<>(unLockStr, Long.class);

        private final String key;

        public LockImpl(String key) {
            this.key = key;
        }

        //尝试加锁，立即返回
        @Override
        public boolean tryLock(String uuid) {
            Long lc = redisTemplate.execute(lockScript, Collections.singletonList(this.key), uuid, String.valueOf(keyTimeOut));
            log.info(uuid + " 试图加锁");
            return lc != null && lc == 1;
        }

        //自旋锁
        @Override
        public boolean tryLock(String uuid, long timeOut, TimeUnit timeUnit) {
            if (timeOut < 0) throw new IllegalArgumentException("timeOut is illegal");
            final long allTime = timeUnit.toMillis(timeOut);
            long start = System.currentTimeMillis();
            while (start + allTime >= System.currentTimeMillis()) {
                if (this.tryLock(uuid)) return true;
                try {
                    Thread.sleep(SLEEP_TIME);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return false;
        }

        @Override
        public void lock(String uuid) {
            while (true) {
                if (this.tryLock(uuid)) return;
                try {
                    Thread.sleep(SLEEP_TIME);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void unLock(String uuid) {
            redisTemplate.execute(unLockScript, Collections.singletonList(this.key), uuid);
            log.info(uuid + " 释放锁");
        }
    }
}